<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>正则案例归纳</title>
</head>

<body>

  <script src="./案例.js"></script>

  <!-- 

        正则：
            匹配模式 要么匹配字符 要么匹配位置；
            可使用小括号捕获数据 要么在API中进行分组 要么在正则里进行反向引用；

        一、字符匹配：
            1.两种模糊匹配：
                横向模糊匹配：
                    匹配的字符串长度不是固定的，可以是多种情况；
                    使用量词 {m,n} 实现； /ab{2,5}c/
                纵向模糊匹配：
                    匹配长度相同的时候 其中每个字符 可能不一样 有多种情况；
                    使用 [] 实现： /a[123]b/

            2.字符组：
                范围表示：[abcdefg...z] = [a-z] 取值 a到z；
                排除字符组：[^abc] 取反 排除 abc 的匹配；

                常见的简写形式：
                    数字： \d === [0-9]     \D === [^0-9]

                    单词字符： \w === [0-9a-zA-Z_]      \W === [^0-9a-zA-Z_]

                    空白符： \s === [ \t\v\n\r\f]        \S === [^ \t\v\n\r\f]

                    通配符： . 几乎任意字符  === [^\n\r\u2028\u2029]  除了 换行、回车、航分隔符、段分隔符

            3.量词：
                其实就是重复字符 {m,n} 表示至少重复 m 次 最多重复 n 次

                简写：
                    {m,} 至少出现 m 次
                    {m} 精确出现 m 次
                    ? === {0,1} 要么有，要么没有
                    + === {1，} 至少出现一次
                    * === {0, } 出现任意次
                    
                贪婪模式：尽可能多的匹配 {2,5} 多多益善...
                惰性模式：尽可能少的匹配 {2,5}? 有两个就不往下数了...

            4.多选分支：
                其实可以理解为 一个大正则表达式里 包含了多个小正则分支 就跟if else分支类似：
                用 | 分隔
                (r1|r2|r3) 表示 按照 r1 或 r2 或 r3的规则去匹配；

                注意点：分支匹配也是惰性的 /good|goodbye/ === /goodbye|good/ === good 匹配到good就不会往下匹配了；

        二、位置匹配：
            1.位置：相邻的字符之间的位置 ''h''e''l''l''o'' 其实就是空字符...

            2.配置的匹配方式：
                ^ 字符串开始的位置(放在中括号中表示取反)；多行中 匹配行开头
                $ 字符串结尾的位置；多行中 匹配行结尾；
    
                \b 单词边界 \w~\W \w~^ \w~$ 之间的位置；
                \B 非单词边界 也就是 \w~\w \W~\W ^~\W \W~$ 之间的位置；

                (?=p) 匹配p前面的位置 不包括p本身;                 positive lookahead 正向前置断言
                (?!p) 除了p前面的位置 都匹配 不包括p本身；          negative lookahead 反向前置断言

                (?<=p) 匹配p后面的位置 不包括p本身；               positive lookahead 正向后置断言
                (?<!p) 除了p后面的位置 都匹配 不包括p本身；         negative lookahead 反向后置断言

        三、括号的作用：
            1.分组和分支结构：
                分组：
                    /ab+/ 只能匹配 ab abb abbb... 
                    /(ab)+/ 可以匹配 abab ababab...

                分支：
                    /a b|c d/ 匹配的是 a b 或者 c d
                    /a (b|c) d/ 匹配 a b d 或者 a c d

            2.捕获分组：
                可以进行数据提取、替换或者其他操作
                提取：
                    一个括号就是一个分组 分组可以被单独其取出来

                    /(aa)-(bb)-(cc)/.exec('aa-bb-cc') 返回一个数组 包含了 aa-bb-cc aa bb cc和下标

                    也可使用 $1 $2 $3... 来提取 aa bb cc;

                替换：
                    能提取就能进行替换 使用replace的方式 就能进行替换

                    'aa-bb-cc'.replace(/(a)-(b)-(c)/,$2/$3/$1)  ==> 'bb/cc/aa'

                反向引用：其实就是重复引用
                    在正则表达式中重复引用分组：
                    /(a(b(c(d))))\1\2\3\4/ 
                    \1 就表示此处引用第一个分组 匹配到 abcd
                    \2 就表示此处引用第二个分组 匹配到 bcd
                    \3 就表示此处引用第三个分组 匹配到 cd
                    \4 就表示此处引用第四个分组 匹配到 d

                    如果引用了不存在的分组 那就将字符转义：
                    /\1\2\3\4/ 转义为" \u0001\u0002\u0003\u0004"

                3.非捕获分组：
                    只保留括号最原始的功能 分组不会被 API引用 也不会被正则反向引用
                    /(?:ab)+/g 这里的分组 ab 就不会被提取出来 只是单纯用来匹配 至少一次的 ab 的；

        四、正表达式的拆分：
            结构和操作符：
                操作符是语言的指令 当一堆操作符出现在一起 就要按照一定的规则 优先级去判断 该先执行哪个操作；

                正则中的操作符体现在不同的结构里:
                    字面量：一个具体的字符；
                    字符组：多种可能的字符的分组 [0-9]；
                    量词：一个字符连续出现;
                    锚点：匹配一个位置；
                    分组：一个括号表示一个分组 (ab)+;
                    分支：多选一；
                    反向引用：重复引用分组 \1 \2...;
                
                涉及的操作符：
                    转义：\
                    括号：() []
                    量词限定符：{m} {m,n} {m,} ? + *
                    管道符：|

                优先级：从上到下 由高到低...

                拆分一个正则：
                /ab?(c|de*)+|fg/

                可拆分如下：
                                     ___c________                     
                      ___a___{b}___{|            |  }----|     
                🤣---|              |___d___{e}__|       |---😁     
                     |___fg_______________________________|
            注意事项：
                字符串的整体问题；
                量词连缀问题；
                元字符的转义问题；

        五、正则表达式的构建：
            1.平衡法则：
                匹配预期的字符串；
                不匹配非预期的字符串；
                可读性和可维护性；
                效率；
            2.构建前提：
                能否使用正则？
                有没有必要使用正则？
                是否有必要去构建一个很复杂的正则表达式？ 其实可以拆分成多个小正则，通过函数来完成；

                能用字符串API解决的问题 就不要用正则...
                    
                比如 从日期中提取年月日
                /^(\d{4})-(\d{2})-(\d{2})/.exec('2021-08-05')

                返回的数组包含了很多东西...

                但是采用数组API就很容易：
                '2021-08-05'.split('-') ==> ['2021','08','05']
            3.准确性：
                匹配预期，不匹配非预期

                比如：匹配固定电话
                055188888888

                0551-88888888

                (0551)88888888

                三种情况综合起来 就有三个分支
                /^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/

                提取公共部分之后 可简写为：
                /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

                要先把所有情况、分支，分析清楚，然后再去构建一个简单、易读的正则；

            4.效率：
                保证了准确性之后 再去考虑优化问题
                1.使用具体型字符组代替通配符，消除回溯；
                    回溯匹配原理：简单说就是 找不到就返回上一次的位置重新找；
                        因为它会尽可能多的去搜索所有情况，需要分配内存来保存之前搜素过的情况，
                        以便后续找不到的时候，可以跳回到之前找过的情况；
                    比如：匹配 123"abc"456 中的 "abc"
                        使用通配符： /".*?"/ 
                        这样 第一次匹配到第二个 " 的时候，也会被当做任意字符处理，
                        匹配不会结束，还会往后去找 还有没有 ";
                        找到 6 发现找不到了 就会回溯回去 需要回溯 6 5 4 " 总共四次，造成了内存的浪费；

                        改为：/"[^"]"/ 就可以避免回溯了；
                2.使用非捕获分组：
                    如果括号里的分组 不需要提取 或者 引用，就可以使用 (?:p) 去代替；
                    因为需要引用的分组 要创建内存来保存它；
                3.独立出确定的字符：
                    /a+/ 改成 /aa*/ a+表示必须出现一次以上，那就让这一次 一直存在，
                    接下来只需要判断 a 后面的情况就行了 可以加快判断速度；
                4.提取分支的公共部分：
                    /^this|^that/ 提取公因式： /^th(?:is|at)/
                    可以减少可消除的重复；
                5.减少分支的数量，缩小范围：
                    /red|read/ 可修改为 /rea?d/ 但是这样会降低可读性；
        六、正则表达式编程：结合API来使用
            1.四种操作：
                验证：就是判断是否匹配到预期的值
                    !!~str.search(reg) 将匹配返回的数值强制转化为布尔值
                    !!str.match(reg) 

                    reg.test(str) 返回布尔值 最常用的方法
                    reg.exec(str) 返回一个数组 没有则返回 null

                切分：把目标字符串 切成一段一段的
                    str.split(reg) 返回一个数组

                    'a,b,c'.split(/,/) ==> ['a','b','c']
                    '1-2-3 | 1.2.3 | 1/2/3'.split(/\D/) ==> ['1','2','3']

                提取: 捕获 引用
                    str.match(reg) 匹配到则返回数组 可从数组中提取； 最常用

                    str.search(reg) 通过 Regex.$[1-9] 来提取；

                    str.replace(reg)

                        let regex = /^(\d{4})\D(\d{2})\D(\d{2})$/
                        let  string = "2021-08-05"

                        let date = []

                        string.replace(regex, () => {

                        date.push(RegExp.$1, RegExp.$2, RegExp.$3)

                        })

                        date ==> ['2021','08','05']

                        reg.test(str) 通过 Regex.$[1-9] 来提取；
                        reg.exec(str) 直接返回一个数组 包含了匹配信息；

                替换：替换成其他的格式或者字符
                        str.replace(reg,'想替换的内容')

            2.相关注意点：
                正则操作的常用API共六个：
                    字符串四个：
                        spilt、match、replace、search
                    正则两个：
                        test、exec
                1.search和match的参数问题：
                    这两个方法 会把传入的 字符串转化为正则 如果直接传入字符 可能会出错
                    '1.2'.search('.') ==> 0
                    '.' 被转化为 /./
                    需要改写为： '\\.' <==> /\./
                2.match返回结果的格式问题：
                    这个跟 正则修饰符 g 有关
                    g 表示全局匹配 返回的结果 包含的内容就比较多；
                    没有就返回null
                3.exec比match更强大，能够接着上一次匹配的结果继续匹配；
                    通常结合while来使用
                    let str = '...'
                    let reg = /.../g
                    while(r = reg.exec(str)){
                        log(r)
                    }
                4.修饰符 g 对 exec 和 test 的影响：
                    有了 g 之后 两个API就会接着上一次的匹配结果去匹配
                    没有 g 则每次都从 0 位开始匹配；
                5.split注意事项：
                    第二个参数 split(reg,3) 表示返回的数组长度 为3 多余的舍弃；
                    使用正则分组时，分组会被捕获到数组里；
                6.replace的第二个参数：
                    可以是字符串 也可是回调函数；
                    以下字符含有特殊意义：
                        $1~$99 表示 1~99个捕获的分组的内容；
                        $& 表示匹配到的子串文本
                        $` 子串的左边文本
                        $’ 子串的右边文本
                        $$ 美元符号

                        例如，把"2,3,5"，变成"5=2+3"：
                        "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2")

                        把"2,3,5"，变成"222,333,555":
                        "2,3,5".replace(/(\d+)/g, "$&$&$&")

                        把"2+3=5"，变成"2+3=2+3=5=5":
                        "2+3=5".replace(/=/, "$&$`$&$'$&")

                        当第二个参数是函数时，我们需要注意该回调函数的参数具体是什么：
                        "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, (match, $1, $2, index, input)=> {

                        console.log([match, $1, $2, index, input])

                        ==> ["1234", "1", "4", 0, "1234 2345 3456"]

                        ==> ["2345", "2", "5", 5, "1234 2345 3456"]

                        ==> ["3456", "3", "6", 10, "1234 2345 3456"]

                        })

                7.尽量使用字面量创见正则，构造函数比较麻烦：
                    使用构造函数 主要是用来进行 动态拼接正则
                    let r = /.../
                    let reg = new RegExp(r,'g')
                    
                    同时 使用构造函数 会多写很多 \ 因为参数会被解析为字符串 需要使用狠多 转义；

                8.修饰符：
                    三个：
                        g 全局匹配
                        i 忽略大小写
                        m 多行匹配 只影响 ^ $

                        正则对象 有三者相应的只读属性：
                        .global .ignoreCase .multiline
                        设置了修饰符就为 true 否则为fasle;

                9.source属性：
                    构建动态正则的时候 可以通过该属性查看构造出来的正则是什么：
                       var className = "high";

                       var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");

                       console.log( regex.source )

                       ==> (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"
                
                10.构造函数属性：
                    构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是$1,...,$9之外，还有几个不太常用的属性（有兼容性问题）：

                    RegExp.input 最近一次目标字符串，简写成 RegExp["$_"]
                    RegExp.lastMatch 最近一次匹配的文本，简写成 RegExp["$&"]
                    RegExp.lastParen 最近一次捕获的文本，简写成 RegExp["$+"]
                    RegExp.leftContext 目标字符串中 lastMatch之前的文本，简写成 RegExp["$`"]
                    RegExp.rightContext 目标字符串中 lastMatch之后的文本，简写成 RegExp["$'"]
     -->


</body>

</html>